
package app;

import java.awt.*;
import java.awt.geom.GeneralPath;
import java.io.*;
import java.util.*;

/**
 * Klasa przechowywująca dane trasy stworzonej przez użytkownika.
 * Udostępnia także funkcje skalowania trasy.
 * @author CygiDawid
 */
public class Track implements java.io.Serializable {
    private final ArrayList<Point> _points;
    private String _path;
    private final double _minDistance = 1d;
    private int _minX;
    private int _maxX;
    private int _minY;
    private int _maxY;
    private double _scale = 1d;
    
    
    public Track() {
        _points = new ArrayList();
    }
    
    
    /**
     * "Setter" ścieżki do pliku z trasą
     * @param path Ścieżka do pliku
     */
    void setPath(String path) {
        _path = path;
    }
    
    /**
     * "Getter" ścieżki do pliku z trasą
     * @return Ścieżka do pliku z trasą
     */
    public String getPath() {
        return _path;
    }
    
    /**
     * Sprawdza, czy trasa posiada już jakieś punkty.
     * @return Warunek określający, czy trasa ma już co najmniej jeden punkt
     */
    public boolean hasStart() {
        return _points.size() > 0;
    }
    
    /**
     * Określa prostokąt, który zawiera trasę.
     * @return Prostokąt, w którym mieści się trasa
     */
    public Rectangle getTrackRect() {
        return new Rectangle(_minX, _minY, _maxX - _minX, _maxY - _minY);
    }
    
    /**
     * Pobiera punkt trasy wysunięty najbardziej do góry i na lewo
     * @return Lewy-górny punkt trasy
     */
    public Point getTopLeftPoint() {
        return new Point(_minX, _minY);
    }
    
    /**
     * Pobiera punkt trasy wysunięty najbardziej na dół i na prawo
     * @return Prawy-dolny punkt trasy
     */
    public Point getBottomRightPoint() {
        return new Point(_maxX, _maxY);
    }
    
    /**
     * Pobiera ilość punktów na trasie
     * @return Ilość punktów na trasie
     */
    public int getPointsCount() {
        return _points.size();
    }
    
    /**
     * Pobiera punkt z trasy o podanym indeksie (indeksy są w kolejności
     * dodawania punktów do trasy)
     * @param idx Indeks określony przy dodawaniu punktu do trasy
     * @return Punkt z trasy o podanym indeksie
     */
    public Point getPoint(int idx) {
        return _points.get(idx);
    }
    
    /**
     * Pobiera ścieżkę określającą trasę z dotychczas stworzonych punktów
     * @return Ścieżka graficzna
     */
    public GeneralPath getActualPath() {
        GeneralPath path = new GeneralPath();
        
        if (hasStart())
        {
            Point p = _points.get(0);
            path.moveTo(p.x, p.y);

            for (int i = 1; i < _points.size(); i++) {
                p = _points.get(i);
                path.lineTo(p.x, p.y);
            }
        }
        
        return path;
    }
    
    /**
     * Określa, czy trasę można zamknąć; Trasa może być zamknięta (połączona),
     * gdy ma conajmniej 3 punkty
     * @return Warunek, czy trasa może być zamknięta
     */
    public boolean closeTrack() {
        if (_points.size() < 3) {
            return false;
        }
        
        return true;
    }
    
    /**
     * Dodanie nowego punktu do trasy; Jeśli odległość pomiędzy nowym
     * punktem, a ostatnim jest zbyt mała punkt nie zostaje dodany do trasy
     * @param point Nowy punkt
     * @return Poprawność dodania punktu
     */
    public boolean addPoint(Point point) {
        if (hasStart()) {
            Point last = _points.get(_points.size() - 1);
            if (getDistance(last, point) < _minDistance) {
                return false;
            }
            
            updateBounds(point);
        }
        else
        {
            _minX = _maxX = point.x;
            _minY = _maxY = point.y;
        }
        
        _points.add(point);
        return true;
    }
    
    /**
     * Uaktualnia najbardziej wysunięte na boki punkty trasy, aby obliczyć granice
     * @param p Nowy punkt do sprawdzenia granic
     */
    private void updateBounds(Point p) {
        if (p.x < _minX) {
            _minX = p.x;
        }
        else if (p.x > _maxX) {
            _maxX = p.x;
        }
        
        if (p.y < _minY) {
            _minY = p.y;
        }
        else if (p.y > _maxY) {
            _maxY = p.y;
        }
    }
    
    /**
     * Wyskalowanie trasy, aby zmieścić ją w określonym obszarze
     * @param size Obszar, w którym trasa zostanie umieszczona
     * @param offset Odstęp trasy od granic obszaru
     */
    public void fitTo(Dimension size, int offset) {
        double width = (double)(_maxX - _minX);
        double height = (double)(_maxY - _minY);
        double scaleX = (double)(size.width - offset) / width;
        double scaleY = (double)(size.height - offset) / height;
        int minX = _minX;
        int minY = _minY;
        int offsetX = offset / 2;
        int offsetY = offsetX;
        
        if (scaleX < scaleY) {
            _scale = scaleX;
            offsetY = ((size.height - (int)(height * _scale)) / 2);
        }
        else {
            _scale = scaleY;
            offsetX = ((size.width - (int)(width * _scale)) / 2);
        }
        
        Point p = _points.get(0);
        p.x = (int)((p.x - minX) * _scale) + offsetX;
        p.y = (int)((p.y - minY) * _scale) + offsetY;
        _minX = _maxX = p.x;
        _minY = _maxY = p.y;
        
        for (int i = 1; i < _points.size(); i++) {
            p = _points.get(i);
            p.x = (int)((p.x - minX) * _scale) + offsetX;
            p.y = (int)((p.y - minY) * _scale) + offsetY;
            
            updateBounds(p);
        }
    }
    
    
    /**
     * Wczytanie trasy z pliku
     * @param path Ścieżka do pliku z trasą
     * @return Nowy obiekt trasy wczytany z pliku
     */
    public static Track load(String path) {
        Track result;
        FileInputStream file = null;
        ObjectInputStream input = null;

        try
        {
            file = new FileInputStream(path);
            input = new ObjectInputStream(file);
            result = (Track) input.readObject();
        }
        catch(IOException | ClassNotFoundException i)
        {
            System.err.println(i.getStackTrace());
            return null;
        }
        finally {
            try {
                if (input != null) {
                    input.close();
                }
            } catch (IOException e) { }
            try {
                if (file != null) {
                    file.close();
                }
            } catch (IOException e) { }
        }
        
        result.setPath(path);
        return result;
    }
    
    /**
     * Zapisanie trasy do pliku
     * @param path Ścieżka do pliku, gdzie trasa ma zostać zapisana
     * @return Poprawność zapisania trasy do pliku
     */
    public boolean save(String path) {
        FileOutputStream file = null;
        ObjectOutputStream output = null;
        
        try
        {
            file = new FileOutputStream(path);
            output = new ObjectOutputStream(file);
            output.writeObject(this);
        }
        catch(IOException i)
        {
            System.err.println(i.getStackTrace());
        }
        finally {
            try {
                if (output != null) {
                    output.close();
                }
            } catch (IOException e) {  }
            try {
                if (file != null) {
                    file.close();
                }
            } catch (IOException e) {  }
        }
      
        _path = path;
        return false;
    }
    
    /**
     * Wyliczenie odległości pomiędzy podanymi punktami
     * @param point1 Punkt pierwszy
     * @param point2 Punkt drugi
     * @return Odległość pomiędzy punktami
     */
    private static double getDistance(Point point1, Point point2) {
        return Math.sqrt(Math.pow(point1.x - point2.x, 2d) + Math.pow(point1.y - point2.y, 2d));
    }
}
